package com.luo.demo.scg.client.config;

import com.luo.demo.scg.client.ext.RefreshTokenErrorMapReactiveOAuth2AuthorizedClientManager;
import com.luo.demo.scg.client.ext.SaveRequestServerOAuth2AuthorizationRequestResolver;
import com.luo.demo.scg.client.ext.UserInfoRelayGatewayFilterFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * SpringSecurity OAuth2 Client配置
 *
 * @author luohq
 * @version 1.0.0
 * @date 2022-02-21 10:14
 */
@Configuration
public class Oauth2ClientSecurityConfig {

    /**
     * 需要进行OAuth2 Resource验证的API模式列表
     */
    @Value("${spring.security.oauth2.resourceserver.api-path-mvc-patterns: @null}")
    private String[] apiPathMvcPatterns;

    /**
     * TODO 后续支持可配置（或者自动提取）
     */
    private String oauth2LoginEndpoint = "/oauth2/authorization/scg-client-prod";
    private String postLogoutRedirectUriPath = "/logout_result";
    private String postLogoutRedirectUri = "https://scg-client-prod:8080" + postLogoutRedirectUriPath;

    @Bean
    SecurityWebFilterChain oauth2ClientSecurityFilterChain(ServerHttpSecurity http,
                                                           ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver,
                                                           OidcClientInitiatedServerLogoutSuccessHandler oidcClientInitiatedServerLogoutSuccessHandler) throws Exception {
        http
                .headers(headers -> headers
                        .frameOptions(frameOptions -> frameOptions
                                .disable()
                        )
                )
                //设置request matcher(仅针对需要进行OAuth Resource验证的API进行拦截)
                .securityMatcher(this.buildExchangeMatcher())
                .exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
                        //自定义登录端点 - Ajax请求401，OPTIONS请求200，其他默认OAuth2 302 Redirect登录
                        .authenticationEntryPoint(this.buildAjax401AndHtml302AuthEntryPoint())
                )
                //禁用CSRF token，否则post请求会被拦截返回403
                .csrf().disable()
                //授权设置
                .authorizeExchange(authorize -> authorize
                        //.pathMatchers("/logout", "/front_logout").permitAll()
                        //集成自定义验证器
                        .anyExchange().authenticated()
                )
                //.oauth2Login(Customizer.withDefaults())
                .oauth2Login(oAuth2LoginSpec -> oAuth2LoginSpec
                                //认证失败原默认redirect /login?error
                                //现设置为认证入口点 - Ajax请求401，OPTIONS请求200，其他默认OAuth2 302 Redirect登录
                                .authenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(this.buildAjax401AndHtml302AuthEntryPoint()))
                                //覆盖AuthorizationRequest解析实现 - 提取redirect_uri参数并转换为request保持到session中，后续回调到此redirect_uri
                                .authorizationRequestResolver(saveRequestServerOAuth2AuthorizationRequestResolver)
                        //使用Session存储已授权的客户端信息
                        //.authorizedClientRepository(new WebSessionServerOAuth2AuthorizedClientRepository())
                )
                .oauth2Client(Customizer.withDefaults())
                //.oauth2Client(oAuth2ClientSpec -> oAuth2ClientSpec
                //        .authorizedClientRepository(new WebSessionServerOAuth2AuthorizedClientRepository())
                //)
                .logout(logout -> logout
                        //默认只能接收POST /logout
                        //此处修改为接收GET /logout
                        .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))
                        .logoutSuccessHandler(oidcClientInitiatedServerLogoutSuccessHandler)
                );
        return http.build();
    }

    /**
     * 自定义登录端点 - Ajax请求401，OPTIONS请求200，其他默认OAuth2 302 Redirect登录
     */
    private ServerAuthenticationEntryPoint buildAjax401AndHtml302AuthEntryPoint() {
        //请求头accept为application/json且忽略*/*
        MediaTypeServerWebExchangeMatcher applicationJsonMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.APPLICATION_JSON);
        applicationJsonMatcher.setIgnoredMediaTypes(Stream.of(MediaType.ALL).collect(Collectors.toSet()));
        List<DelegatingServerAuthenticationEntryPoint.DelegateEntry> delegateEntryList = Arrays.asList(
                //请求头accept为application/json -> 返回401
                new DelegatingServerAuthenticationEntryPoint.DelegateEntry(
                        applicationJsonMatcher,
                        new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)),
                //请求头X-Requested-With为XMLHttpRequest -> 返回401
                new DelegatingServerAuthenticationEntryPoint.DelegateEntry(new ServerWebExchangeMatcher() {
                    @Override
                    public Mono<MatchResult> matches(ServerWebExchange exchange) {
                        String xRequestedWith = exchange.getRequest().getHeaders().getFirst("X-Requested-With");
                        Boolean match = StringUtils.hasText(xRequestedWith) && xRequestedWith.equals("XMLHttpRequest");
                        return match ? MatchResult.match() : MatchResult.notMatch();
                    }
                }, new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)),
                //跨域OPTIONS请求返回200（解决浏览器报错）
                new DelegatingServerAuthenticationEntryPoint.DelegateEntry(new ServerWebExchangeMatcher() {
                    @Override
                    public Mono<MatchResult> matches(ServerWebExchange exchange) {
                        HttpMethod method = exchange.getRequest().getMethod();
                        Boolean match = HttpMethod.OPTIONS.equals(method);
                        return match ? MatchResult.match() : MatchResult.notMatch();
                    }
                }, new HttpStatusServerEntryPoint(HttpStatus.OK))
        );

        DelegatingServerAuthenticationEntryPoint nonAjaxLoginEntryPoint = new DelegatingServerAuthenticationEntryPoint(delegateEntryList);
        //默认登录入口即为OAuth2重定向登录端点
        nonAjaxLoginEntryPoint.setDefaultEntryPoint(new RedirectServerAuthenticationEntryPoint(this.oauth2LoginEndpoint));
        return nonAjaxLoginEntryPoint;
    }

    /**
     * 全局定义WebFlux AuthorizedClient仓库实现
     */
    @Bean
    public ServerOAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository() {
        return new WebSessionServerOAuth2AuthorizedClientRepository();
    }


    /**
     * 自定义RefreshTokenErrorMapReactiveOAuth2AuthorizedClientManager实现，
     * 支持refresh_token过期后触发登录（避免返回500）
     */
    @Bean
    @Primary
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                                         ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
        return new RefreshTokenErrorMapReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
    }


    /**
     *  OAuth2 Client Authorization Endpoint /oauth2/authoriztion/{clientRegId} <br/>
     *  请求解析器扩展实现 - 支持提取query参数redirect_uri，用作后续OAuth2认证完成后SCG重定向到该指定redirect_uri。<br/>
     *  适用场景：SPA -> SCG -> SCG返回401 -> SPA重定向到/oauth2/authorization/{clientRegId}?redirect_uri=http://spa -> SCG完成OAuth2认证后再重定向回http://spa
     */
    @Bean
    @Primary
    public ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        return new SaveRequestServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
    }


    /**
     * 构建请求匹配器
     *
     * @return 请求匹配器
     */
    private ServerWebExchangeMatcher buildExchangeMatcher() {
        //if (null == apiPathMvcPatterns || 0 >= apiPathMvcPatterns.length) {
            //排除登出结果页面，即被排除页面可以不提供用户cookie任意访问
            return new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(this.postLogoutRedirectUriPath));
            //return ServerWebExchangeMatchers.anyExchange();
        //}
        //return ServerWebExchangeMatchers.pathMatchers(this.apiPathMvcPatterns);
    }

    /**
     * 单点登录配置
     */
    @Bean
    public OidcClientInitiatedServerLogoutSuccessHandler oidcClientInitiatedServerLogoutSuccessHandler(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        OidcClientInitiatedServerLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
        oidcClientInitiatedLogoutSuccessHandler.setPostLogoutRedirectUri(this.postLogoutRedirectUri);
        //若当前SCG Client Session已过期，则无法获取OidcUser信息（id_token），
        //则默认跳转到登出结果页（避免默认重定向到/login?logout）
        oidcClientInitiatedLogoutSuccessHandler.setLogoutSuccessUrl(URI.create(this.postLogoutRedirectUri));
        return oidcClientInitiatedLogoutSuccessHandler;
    }


    /**
     * 自定义UserInfo过滤器工厂
     */
    @Bean
    public UserInfoRelayGatewayFilterFactory userInfoRelayGatewayFilterFactory() {
        return new UserInfoRelayGatewayFilterFactory();
    }
}
